C++ Database Library Version 2.3, 16th August 1995 Package Contents ================ The package contains the following files: SOURCE.ZIP The source files for the library. This file contains the following files: DATABASE.CPP Source file DATAEXE1.CPP Source file DATAEXE2.CPP Source file FIELD.CPP Source file INDCLUS.CPP Source file INDEX.CPP Source file INDEX2.CPP Source file RECORD.CPP Source file UTIL.CPP Source file EXOP.HPP Private header file DATAPRIV.HPP Private header file DATABASE.HPP Header file for all versions of the library DATABASE.TXT User manual for library. README.TXT File list and conditions DBRPT.CPP Example program FAMILYS.DBF Example database FAMILYS.CPP Example program FAMILYS.NDX Index file for example database PACKDB.CPP Example program Library, Conditions for use: ============================ This library of routines is provided as shareware. You may try the functions defined in the library within in your own programs, however if you decide to incorporate them permanently then you are obliged to register your copy of the software. Registration costs £30 UK sterling or $40.00 (by cheque) to: Robin Abbott Compuserve ID 100023,535 37 Plantation Drive, Christchurch, Dorset ENGLAND BH23 5SG Compuserve Registration ======================= To register this library through Compuserve GO SWREG, and follow the instructions there. Use registration number 7118. Compuserve ID 100023,535 Registration brings you the next library upgrade free of charge and technical support. Compiled programs which incorporate the code within these libraries may be distributed with no further royalty. The library may be freely distributed provided that all files are included. Known Bugs, 15/8/95 =================== I have now tested the index routines with 100,000 record databases, generating and changing every single record in the database. I am (fairly!) confident that all the significant bugs have been ironed out, as these tests cover index tree depths of up to 6. The verifyindex() routine has been made much more stringent and shows valid index structures for all the tests I have carried out. Bug reports =========== Send bug reports to the author. I will try to solve bugs and either return an updated software version asap, or provide a work around. Simple bugs can just be reported, e.g. the bug that field names could not have an underscore in them within the expression evaluator. More complex bugs such as all the errors that occurred in the index routines really need to identify the problem on a specific database. This may not be easy, but please try and identify the routines which cause the problem and send the complete database. Introduction ============ This library is intended to allow Borland C++ programs to create, write, interrogate and manipulate databases and indexes which are in dBase III format. These files usually have the extension .dbf. The library is based on a group of objects which represent databases, records and fields. The library of functions will be of use in building database applications and report generators, both standalone, and for use with other database applications. Using the Library in Building Programs ====================================== The package incorporates 9 source files, which must be included in the project build, or which can be built into a library. These files are as follows: DATABASE.CPP Source file (X) DATAEXE1.CPP Source file (X) DATAEXE2.CPP Source file (X) FIELD.CPP Source file (X) INDCLUS.CPP Source file (X) INDEX.CPP Source file (X) INDEX2.CPP Source file (X) RECORD.CPP Source file (X) UTIL.CPP Source file (X) These incorporate all the functions described in this document and should be compiled using the large model. Copy the header file database.hpp to your include directory. Programs which use the library must include the header file database.hpp, and must be compiled using the large model. Using the Package ================= The package is intended to be as easy as possible to use. A database object should be opened with the name of the database in which the user is interested together with the first index file if required. Additional index files may be added as required. A record object (or many records) may now be opened on this database. The record object may be positioned at any record in the file and the fields in the record may be examined or modified. It is possible to select records on a variety of criteria including dBase expressions. The following example shows a program which opens the file "CLUB.DBF" and swaps the month and day for each record in the file. This was written to correct an error in importing a file in American date format. The file has a number of fields, the only one considered here is the DOB field - the date of birth which was incorrectly imported. /* Short Program to exchange Day & Month for every record in a database */ #include #include #include #include void main() { char ws[128]; // Convenient Workspace database db("CLUB"); // Open database, no index record rec(db); // Record open on database int fn=db.getfield("DOB")->getnumber(); // Get number of DOB field int sel=rec.select(FIRST); // Select first record while(!sel) // For every record in the file { int d,m,y; // day, month and year gnums(rec.getfield(fn),d,m,y); // Get date of birth to d,m,y sprintf(ws,"%04d%02d%02d",y,d,m); // Print in db form, swap d&m rec.setfield(fn,ws); // Update field rec.write(); // Write back to disk sel=rec.select(NEXT); // Select next record } } Indexes ======= The package is capable of handling dBase style NDX files (indexes). These are attached using the database::addindex() function. Indexes are referred to by the file name with no path and no extension. Thus the index "c:\c\name.ndx" is referred to using the string "name". Indexes are usually attached when the database is opened, or when they have been created. New records will be included in the index only if the index is attached to the database. Should an out of date index be attached to a database then there is a risk that the records will be out of order, to check a database the database::verifyindex() function may be used to check the index. Big Files ========= Large databases (greater than about 2000 records) need handling carefully to avoid speed problems. Indexes should be built on these databases as little as possible, being maintained by attaching them to the database. Selecting records should be done by using the index key (the record::selkey() function), which will be considerably faster than using any of the record::select() functions - these functions search the whole database from beginning to end. The database::verifyindex() function should also be avoided as this searches the entire database. Errors ====== The database will report fatal errors (by printing the errors on screen) which may cause a crash such as opening a record on an invalid database. Some more trivial errors which may be trapped by the calling program will also be printed. The global variable eroff should be set to 1 if it is required to turn off printing of these non-fatal errors. Fatal errors are printed using printf for DOS compilations, and using MessageBox() for Windows compilations. Objects ======= There are three types of object used by application programs in the database library. Database objects, Field Objects and Record Objects. A database object holds the description of a database, field objects associated with the database hold the definitions of the fields in the database, and record objects hold all the information on a record. Assignment of Objects (Use of the = Operator) ============================================= The = operator may be used within the database library only to copy one record to another (within the same database), this allows the user to save the position and contents of a record within the database, the = operator is implemented to ensure that a complete copy of all structures and variables associated with the record is made. An example of the use of this is shown above in the subtotals program. The = operator must not be used with database objects or field objects, or between records on different databases. See the example program DBPACK below to see how a record may be copied between different databases. DATABASE Objects ================ A database object is constructed with the name of the database to be opened, and an optional index file. The file is opened and the user then has the capability to determine the state of the file and the fields in it. Constructing a database object automatically creates a linked list of the fields in the database. To create a new database a database object is opened without a file name, and then fields of any type can be added to the database. When all the database fields have been written the database can be established by saving it with a filename after which records may be created within it. The database object has the following public interface: class database { public : char ers[81]; // Reports the error in an expression database(char *name,char *index=0); // Construct with file+index database(void); // Construct a new database ~database(void); int addfield(field *newfield); // Copy field to new database int addfield(char *nm,int l=1,int rdp=0); // Add field to new // database int addindex(char *fname); // Add index to the database int buildindex(char *expr,char *fn, void (*prog)(int,long)=0, int ord=100); // Build a newindex field *getfield(int n) {return(fielda[n-1]);} // Return field n, 1<=n<=nfield field *getfield(char *name); // Get ptr to field by name char *getindkey(char *name); // Get index key int getindtype(char *name); // Get index type, OPINT,OPSTR long getnrec(void) {return(nrec);} // Number of records int getreclen(void) {return(reclen);} // Total record length int getnfield(void) {return(nfield);} // Number of fields int getversion(int &maj,int &min); // Get library version int isvalid(void) {return(valid);} // Return valid flag int verifyindex(char *iname,int depth=1, void (*progress)(int test,long recnum)=0, int order=100); // Verify an index int setdateformat(dates form); // Set date format int subindex(char *iname); // Subtract an index } FIELD Objects ============= A field object is created to enable the functions associated with the database to determine the format of the records. It also allows the user to determine the type, name and parameters of any field. The field type has the following public interface: class field { public: field(int number,char *name,char type,int length,int rdp, int recpos); ~field(void); // Accessor functions int getnumber(void) {return(number);} // Get no. of field in record char *getname(void) {strcpy(namecop,name); return(namecop);} // Get name of field char gettype(void) {return(type);} // Get type of field int getlen(void) {return(len);} // Get length of field int getrdp(void) {return(rdp);} // Get right of dp of field }; RECORD Objects ============== There may be any number of record objects associated with each database object. Each record object holds one record in the file, and it is possible to load a record object with the first, last, next, previous, or any numbered record in the database. The record object has the following public interface: class record { public: record(class database &db,char *indname=0); ~record(void); void operator=(record &s); // Copy record entirely int eval(char *expr,void *result,int &rtype); // Evaluate expression int eval(void *result,int &rtype); // Evaluate prev. expression int indchk(char *exp,int &rtype); // check an index expression char *indname(); // Return index name int seldbf(long n); // Select record in dbf int select(long n,int type=NOTDEL,int df=0); // Select rec number int select(int field,int val,long n, int type=NOTDEL); // Select fn==val int select(int field,char *s,long n, int type=NOTDEL); // Select field==s int select(int field,void *s, int (*comp)(void *,void *), long n,int type=NOTDEL); // User def select int select(char *expr,long n, int type=NOTDEL); // Select when expr true char *getfield(int n,int tf=1); // Get field by number char *getfield(char *name,int tf=1); // Get field by name long getrecnum() {return(rn);} // Current record number int getdelstate() {return(delstate);} // Delete state of record void setdelstate(int sval); // Set delete state of record int selkey(char *value,int type=NOTDEL, int num=OPSTR); // Select by key int selkey(double value,int type=NOTDEL); int selkey(); // Find other records by key int setfield(char *name,char *value); // Set new value for field name int setfield(char *name,float value); int setfield(int number,char *value); // Set new value for field by # int setfield(int number,float value); int write(int type=OVER); // Write rec to dbf }; Representation of Field Types ============================= Field types in dBase are Numeric (N), Character (C), Date (D), Logical (L) or Memo (M). They are held internally in the follwoing forms: Numeric ------- Numeric fields are held in double (8 byte) form. When values are returned from the database they are in ASCII form (e.g. 1.235). Thus the C atoi() or atof() functions should be used to convert to integer or floating form as required. Character --------- Character fields are held internally as normal C format 0 terminated strings. Logical ------- Logical fields are held as a single character string (0 terminated) where the value is "T","t","Y", or "y" for true values and "F","f","N", or "n" for false values. Date ---- Date fields are held as character strings (0 terminated) of length 8. These are in the form YYYYMMDD, thus 5th November 1962 is held as "19621105". Memo ---- Memo fields are held as character strings which are 0 terminated, they may be any length up to 64000 bytes provided there is sufficient memory to hold them. Internally memos read from a database are held in a buffer which starts at 512 bytes, and is expanded when necessary to read a larger memo. Expression Evaluator ==================== The expression evaluator in the database library provides a complete dBase compatible expression evaluation function which may be used to select records, may be used for calculations on records, or is used to create index expressions. The expression evaluator is associated with the database object, and there is a public string (ers in the database object) which holds the ASCII reperesentation of the last database error which occurred, see the record::eval function for an example. It provides the following operators which are shown in operator precedence (those at the top of the list are executed first), note that the dBase =< and => (which in dBase are equivalent to >= and <=) are not implemented in the library (use the <= and >= operators instead). () Parenthesis bracket operations and change the order of evaluation. + - Unary + and - operate on a single number. ** ^ Exponentiation (to the power of) * / Multiplication, Division + - Add and Subtract, Numbers, Dates or Strings Addition of two strings concatenates them. Subtraction on strings takes the spaces from the end of the first string, concatenates the second string to the first string, and then puts the spaces back at the end of the new string. A date may have a number added or subtracted from it which provides a date which is that many days in the future or past. A date may be subtracted from another date to give the number of days between the two dates. < > = <> or # <= >= These are relational operators and act on strings, dates, or numbers to indicate if the first item is less than (<), less than or equal to (<=), equal to (=), greater than (>), greater than or equal (>=) or not equal (<>, or #) to the second item. The first item must be of the same type as the second item. $ This is a string operator used in the form A$B, it returns true if A is identical to B, or is contained within it. .AND. Logical AND. .OR. Logical OR. .NOT. Logical NOT, note the lower priority than .AND. and .OR. which can be confusing to those used to conventional operator precedence. Field names be used wherever a number, string or date may be used. e.g. database db("Names"); record rec(db); rec.select("upper(name)=\"B\"",FIRST); // Select 1st record where 1st character of name field is B rec.select("ageyears+agemonths/12<12.5",NEXT); // Select next record where age is less than 12.5 rec.select("dob=ctod(\"17/4/78\")",FIRST); // Select 1st record where dob field is 17th April 1978. Expression Evaluator functions ============================== The following functions are available: ABS(numeric expr) Gives the absolute value of the supplied expression. ASC(string expr) Returns decimal ASCII value of the first character of string AT(char s1,char s2); If string s1 is to be found in string s2 then this returrns the position that it is to be found at (starting at 1). it returns 0 if s2 is not to be found in s1. CDOW(date expr) Returns a string which names the day of the week supplied by date expr. CHR(numeric expr) Gives a string of length 1 holding the character which has the ASCII code supplied by the numeric expression. CMONTH Returns a string which names the month supplied by date expr. CTOD(char expr) Returns a date matching the character string supplied as input. DATE() Returns the current operating system date. DAY(date expr) Returns the number of the day from the supplied date expression. DOW(date expr) Returns a number representing the day of the week from the supplied date expression. DTOC(date expr) Converts the date expression to a charcter string in system date format. DTOS(date expr) Gives the dBase date string form of date expr (YYYYMMDD). Note this function is dBase IV, not dBase III. EXP(numeric expr) Gives the result of e to the power numeric expr. IIF(condition,expr1,expr2) If condition is true returns the result of evaluating expr1, otherwise it returns the result of evaluating expr2. INT(numeric expr) Gives the integer value of the supplied expression. ISALPHA(char expr) Evaluates to TRUE if the 1st character of the supplied expression is a letter of the alphabet, FALSE if not. ISDIGIT(char expr) Evaluates to TRUE if the 1st character of the supplied expression is a number, FALSE if not. ISLOWER(char expr) Evaluates to TRUE if the 1st character of the supplied expression is a lower case letter of the alphabet, FALSE if not. ISUPPER(char expr) Evaluates to TRUE if the 1st character of the supplied expression is an upper case letter of the alphabet, FALSE if not. LEFT(char expr,numeric expr)Returns the numeric expr characters from the left of the supplied string. LEN(char expr) Returns the length of the supplied character expression. LOWER(char expr) Returns a string where all letters of the supplied expression have been converted to lower case. LTRIM(char expr) Returns a string which is the supplied string with all leading spaces removed. MAX(expr,expr) Returns the greater of the two supplied numbers or dates. MIN(expr,expr) Returns the lesser of the two supplied numbers or dates. MOD(numeric expr,numeric expr) Gives the remainder when the first expression is divided by the second. MONTH(date expr) Returns the number of the month represented by the supplied date expression. RECCOUNT() Gives the number of records in the database. RECNO() Gives the record number (in the dbf file)of the record on which the function is evaluated. RECSIZE() Gives the length of the records in the database. REPLICATE(char expr,num expr) Returns a string made by num expr repeats of the supplied character expression. RIGHT(char expr, num expr) Returns the numeric expr characters from the left of the supplied string. ROUND(num expr,num dec) Gives the result of rounding off the supplied numeric expression to dec decimal places. RTRIM(char expr) (Same as TRIM). SOUNDEX(char expr) Gives the string of lenght 4 which results from running the SOUNDEX algorithm on the supplied character expression. Note that this is a dBase IV function and is not supported in dBase III. SPACE(num expr) Returns a string consisting of num expr space characters. STR(number, len, decimal) Returns the string which represents the number supplied. len is the total length of the resulting string, decimal is the number of decimal places. STUFF(str 1,start,len,str 2) Gives a sting which is created by taking str 1, removing the substring which is defined by start and length, and inserting str2 where the substring was removed. SUBSTR(string,start,length) Gives the string which is a substring of the supplied string starting at position start and going on for length characters. If length is not supplied then it returns the rest of the string. The string is assumed to start at position 1. SWAPDATA(string) This is a PCF function which takes the supplied string and takes the first ~ character in it exchanging the information after the ~ for the information before it, and replacing the ~ by a space. Thus "SMITH~JOHN" becomes "JOHN SMITH". This is used in PCF for alphabetical sorting by surname but printing by proper name. This function is NOT dBase III compatible. TIME() Gives the character representation of the current time. TRIM(string expr) Gives the string which results from removing all trailing spaces from the supplied string. TYPE(expr) Gives a single character string which represents the result type of expr. (C, N, L, or D). Note that unlike dBase date expressions are allowed, and that memo fields return C, not M like they would in dBase. UPPER(string expr) Returns a string where all letters of the supplied expression have been converted to upper case. VAL(char expr) Gives the numeric value of the expression contained in char expr. Note that the expression is a simple number thus "4.5" returns the number 4.5, but "9/5" returns the number 9. YEAR(date expr) Gives a four digit number which is the year of the date expression. Examples ======== PACKDB ====== The following example shows a program which packs a database. That is it takes an input database and creates an identical output database which has deleted records removed. The program is invoked using the command line: PACKDB INFILE OUTFILE [INDEX] INFILE is the name of the input database OUTFILE is the output database which will be created. INDEX is the name of the optional index file which will be used to order the input file so that the records are written in this order to the output file. #include #include #include #include #include main(int argc, char *argv[]) { char index[128]; // Holds index name if supplied long i; if (argc<3) // Error need at least two arguments { printf("\nUsage : packdb infile outfile [index]\n"); printf("\n\nDuplicates infile database to outfile database"); printf("\nremoving deleted records.\n\n"); printf("\nindex if supplied will operate on infile to define\n"); printf("\nthe order records are written to outfile\n\n"); exit(1); } if (argc==4) strcpy(index,argv[3]); else *index=0; // Copy index database *dbin=new database(argv[1],index); // Open input database database *dbout=new database(); // Create new output database if (dbin->isvalid()) // Check if input database is valid { delete dbin; delete dbout; printf("\nUnable to open input database, error %d\n\n",dbin- >isvalid()); exit(1); } for(i=1; i<=dbin->getnfield(); i++) // Copy all fields dbout->addfield(dbin->getfield(i)); if (dbout->write(argv[2])) // Check database successfully written { delete dbin; delete dbout; printf("\nUnable to open output database\n\n"); exit(2); } record *reci=new record(*dbin); // Record on old database record *reco=new record(*dbout); // Record on new database long recn=1; printf("\n"); int rv=reci->select(FIRST); // set to 1st record (default undeleted) while(!rv) { for(i=1; i<=dbin->getnfield(); i++) // Copy all fields { if (dbin->getfield(i)->gettype()=='N') // Numeric fields { reco->setfield(i,atof(reci->getfield(i))); } else reco->setfield(i,reci->getfield(i)); // All other fields } reco->write(NEW); // Write new record rv=reci->select(NEXT); // Next input record printf("\r%ld",recn++); } delete reci; delete reco; delete dbin; delete dbout; printf("\n\nOperation complete\n"); } FAMILYS ======= The second example below shows the use of sub totals. This example is on a database of families which has been indexed on the field name SURNAME. The program finds all records which have the SURNAME field set to the same value in the database and works out the average of the field called AGE (age in years) and AGEM (age in months) within the family. It then returns and for every member of the family updates the avgage field which is the difference between the AGE and AGEM fields in the record and the average age worked out for the family. /* Demonstration of Subtotals, Short program to update average age for each family in familys.dbf The average age is a field in each record which will indicate how far away each family member is in age from the average age in that family Index expression in familys.ndx is : UPPER(SURNAME) Thus all family members will be grouped together */ #include #include #include #include #include void main() { char ws[128]; // Workspace database db("familys","familys"); // Open database record rec(db); // Current record record first(db); // Used to save position int fn=db.getfield("avgage")->getnumber(); // Field no. avgage field int sel=rec.select(FIRST); // Select first record do { char surname[128]; // Holds current surname double avgage=0; // Holds average age first=rec; // Save position int ssel=sel; // save sel at current position int trec=0; // Total no. of records in patrol strcpy(surname,rec.getfield("surname")); // Copy in patrol name while(!sel && !strcmp(surname,rec.getfield("surname"))) // Until new name { avgage+=atof(rec.getfield("age"))+atof(rec.getfield("agem"))/12; trec++; sel=rec.select(NEXT); // On to next record } avgage/=trec; while(!ssel && !strcmp(surname,first.getfield("surname"))) // Round again { double age; age=atof(first.getfield("age"))+atof(first.getfield("agem"))/12; first.setfield(fn,int(age-avgage)); first.write(); // Write the new record ssel=first.select(NEXT); } printf("\nAverage age of %s is %f",surname,avgage); } while(!sel); } DBRPT ===== The following program takes a database and prints a report on it, printing number and length of records, and type and length of each file. With the -c option it will print an include file for use in a C++ program which contains "#define" statements for each field number. It takes one argument, the file name with an optional -c to print the the include file. #include #include #include #include main(int argc, char *argv[]) { char *wsp,ws[128]; // Useful work space int inc=0; // Flags results are to be printed in #include form if (argc<2) // Must have at least one parameter { printf("Usage : dbrpt filename [-c]\n"); printf(" -c option -> #include form for header files\n"); exit(1); } if (argc>2) if (!strcmpl(argv[2],"-c")) inc=1; strcpy(ws,argv[1]); strupr(ws); if (wsp=strchr(ws,'.')) *wsp=0; if (wsp=strchr(ws,'\\')) *wsp=0; // Open database database *db=new database(argv[1]); if (db->isvalid()) // Check if failed to open { delete db; printf("\nError on opening database !"); exit(1); } // Dump database stats. if (inc) printf("// Database %s, include file\n\n// ",ws); else printf("\n"); printf("Database : %s has %ld records of length %d with %d fields\n", ws,db->getnrec(),db->getreclen(),db->getnfield()); // Dump field stats for each field for(int i=1; i<=db->getnfield(); i++) { field *fld; fld=db->getfield(i); if (inc) printf("\n#define %-10s %3d // ",fld->getname(),i); else printf("\nField %3d - %-10s ",i,fld->getname()); printf("Type %c, Length %3d, Rdp %d", fld->gettype(),fld->getlen(),fld->getrdp()); } printf("\n\n"); delete db; exit(0); } Global Utility Functions ======================== The following are global functions available under the database library, they may be of use in any database application. char *getdate(time_t time); This function obtains a time in the dbf form from an ANSI C time, the returned string is static and is overwritten by each call to getdate. Thus the current date can be obtained by: #include #include time_t ntime; time(&ntime); char *dp=getdate(ntime); time_t gettime(char *string); This function obtains a time in the form used by the ANSI time functions (e.g. asctime() etc). The string should be in dBase form, YYYYMMDD. void gnums(char *date,int &d,int &m,int &y); This function takes the supplied date and returns the day, month and year functions in the variables d,m and y. See the first example shown above. char *ltrim(char *string); This function returns a pointer to the first character in string which is not a space. char *soundex(char *dest,char *src); This function runs the soundex algorithm on the string src and places the 4 character result in dest which is returned by the function. int strcmpdb(char *s1,char *s2,int len=-1); Compare strings using the dBase compare format. (i.e. "Bancroft"="B" returns TRUE, "B"="Bancroft") returns false. len is the maximum length to be compared, if -1 is supplied then the whole of s1 is examined. char *swapdata(char *dest,int schar='~'); This is a PCF function which takes the supplied string and takes the first ~ (or whichever character is defined by schar) character in it exchanging the information after the ~ for the information before it, and replacing the ~ by a space. Thus "SMITH~JOHN" becomes "JOHN SMITH". This is used in PCF for alphabetical sorting by surname but printing by proper name. The function returns a pointer to dest. char *trim(char *string) char *rtrim(char *string) These functions which are identical trim all trailing spaces from the supplied string and return a pointer to it. Classes ======= The following are the classes which make up the library , and their associated member functions and variables. database::database void database::database(); Description : This constructor creates a database object with no fields and no records. The valid flag is set to -1 to indicate that the database has not yet been written to disk. Fields may be added to the database using the database::addfield() function. Before records may be written or read to or from the database then the database::write() function is used to establish it on disk. E.G. // Create a database : "CLUB" with three fields, name (character) // dob (date), and phone (character) #include database db(); db->addfield("NAME",'C',15); // Name field db->addfield("DOB",'D'); // DOB field db->addfield("PHONE",'C',11); // Phone Field db->write("CLUB"); database::database void database::database(char *name,char *index=0); Description : This constructor opens an existing database which may have 0 or more records. The database constructor requires a file name for the database, and an optional file name for the dBase compatible index. If the index file name is supplied as an empty string, or if the index file cannot be opened, then each record follows the last in the database file. If an index file name is supplied then each record follows the last according to the index file sequence. After the constructor is called the database::isvalid() function may be called to determine if the database was successfully opened. Additional indexes may be added using the database::addindex() function The database filename is assumed to have a ".DBF" extension if none is supplied, and the index filename a ".NDX" extension. If the database has an associated memo file (.DBT extension) then this file is also opened. The constructor automatically creates and initialises a linked list of field objects which describes the fields of the database. Should the constructor fail then the database::isvalid() function will report the error, returing 0 on success. E.G. #include database *db; db=new database("MEMBERS","NAME"); cout << *db; database::Accessor Functions long database::getnrec(void) unsigned int database::getreclen(void); char *getindkey(name); int getindkey(name); int database::getnfield(void); field *database::getfield(int n); void database::getversion(int &major,int &minor); field *database::getfield(char *name); int database::isvalid(void); Description : These are the accessor functions for the database, and return various parameters about it. char *database::getindkey(char *name) This function returns the key on the index which has the supplied file name. name should not have a .NDX extension. The key should be copied if any modification is to be made to it. It returns 0 if no index of that name exists. int database::getindtype(char *name) This function returns the type of index for the index which has the supplied file name. name should not have a .NDX extension. The returned value is either OPINT or OPSTR depending on whether it is an integer or string index expression. It returns 0 if no index of the supplied name exists. int database::getnfield(void); This function returns the number of fields in the database. long database::getnrec(void) This function returns the number of records in the database. unsigned int database::getreclen(void); This function returns the record length of records in the database. void database::getversion(int &major,int &minor); This function sets major to the major number of the database library version and minor to the minor number. For instance version 2.3 will set major to 2, and minor to 3. int database::isvalid(void); This function returns a code which represents the state of the database object. The code takes on the following values: -2 Creating Database with a memo field, not yet written to disk -1 Creating Database, not yet written to disk 0 Database file opened and initialised successfully. NOFILE Database file could not be opened. NOINDEX Index file could not be opened. NOMEM Memory allocation error on database creation. E.G. #include database db("CLUB","") printf("Database has :\n"); printf("%d Records\n",db.getnrec()); printf("%d Fields\n",db.getnfields()); database::addfield int database::addfield(char *name,int type,int length=1,int rdp=0); int database::addfield(field *fp); Description : This function adds a field to a database which is being created. The first form of the function adds a defined field. name is the name of the field, type is the field type - 'C','D','M','L' , or 'N'. Length is defined for character and numeric fields and is the length of these fields in the database. The other field types are set automatically and the length parameter is ignored. The rdp parameter is only used by numeric fields to set the number of decimal places in the field. The second form of the function makes a new field from a pointer to an existing field from an existing (but different) database. The function returns 0 if successful, DUPFIELD if the name of this field exists in the database already, and INVFIELD if a different error occurs. E.G. See the database::database() function. database::addindex field *database::addindex(char *filename); Description : This function adds an index to the list of indexes which is maintained for each database. When an index is added it is available for use by records which may select any index, which then defines the order in which records are read from the database. Any index attached to a database is automatically updated as records are modified or added within the database. The file name is assumed to have a .NDX extension. Functions such as the record constructor which require to identify an index on the database use the name defined in the database constructor or the database::addindex() function. Thus the index "NAME.NDX" is referred to as "NAME" in functions which need to identify an index. The function returns 0 if the index is successfully attached to the database, and the error NOINDEX if the file could not be opened. It returns the error INVIND if there is an error in the index file or if it is already attached. Index file checking is limited and will not spot all types of index file corruption. E.G. #include database *db; db=new database("MEMBERS","AGE"); db.addindex("AGE"); db.addindex("FUNC"); db.addindex("PHDIG"); record rec(db,"FUNC"); // rec uses the FUNC index database::buildindex void database::buildindex(char *expr,char *filename, void (*progress)(int s,long e)=0, int order=250); Description : This function constructs a new index on the database. expr is the expression that is to be used for the index file, it must be a valid index expression. filename is the name of the index file to be written, if a file exists it will be overwritten. Index file creation may take some considerable time for a large database. After a successful creation the index can be attached to the database using the database::addindex() function if it is desired to use it immediately. progress is the optional address of a function which can show the progress of the index build, and can be used for example, to show a bar chart of % complete. The function progress will be called every order records (as default order=250). The function is called with an integer s, which is the stage of the build, and a long e, which is the event. Stage runs from 1 to 3. When stage is 1, then event will count from 1 to the N which is the number of records. When stage is 2 then event will count from 1 to a maximum value which is calculated as N/2*log(N)/log(2), however it may be a lot less depending on how well sorted the records are in the database. When stage is 3 then event will count from 1 to the number of index clusters, it is usually not worth displaying event when stage is 3, as it is so fast. You may find that there are periods in stage 2 of the build (which is the sorting phase), when event is updated very slowly (for instance the first update may take a long time). This is normal. buildindex returns 0 on success, or the error EXPRERR if an error occurred in expression evaluation on any record, or the error NOINDEX if the file could not be opened. The filename must not be already attached to the owning or any other database or the program will almost certainly crash. E.G. #include void prtbuild(int,long); database db=database("MEMBERS"); db.buildindex("UPPER(NAME)","NAME",prtbuild); db.addindex("NAME"); void prtbuild(int stage,long event) { printf("\rStage %d, Event %ld",stage,event); } database::getfield field *database::getfield(int n); field *database::getfield(char *name); Description : These functions return a pointer to the field class which is defined either by number n, or by its name. If the field cannot be found a zero pointer is returned. E.G. #include field *fpt; database db("LIST","NAME"); fpt=db.getfield("NAME"); printf("\nName field is number : %d",fpt->getnumber()); printf("\nName field is of length : %d",fpt->getlen()); database::subindex int database::subindex(char *name); Description : This function removes an index from the list of indexes which is maintained for each database. The name is the filename which was used to add the index (from the database::database() or database::addindex() functions). The function returns 0 on success, or the error NOINDEX if the index could not be found. It will return INDINUSE if a record is using the index and the index will not be removed. database::setdateformat void database::setdateformat(dates newform); Description : This function sets the date format to be used by the expression evaluator. If newform is USDATE then the US date format is used, mm/dd/yyyy, if newform is UKDATE then the european date format is used : dd/mm/yyyy. The default is USDATE. E.G. db->setdateformat(UKDATE); database::write int database::write(char *filename); Description : This function will write a database which is newly created to disk. filename is the name of the database (without the .dbf extension) on disk. After this function has been executed successfully the database will be valid (the valid flag will be 0), and records may be written (and then read) on the database. It returns 0 on success, or NOFILE if the database is not newly created or cannot be written to disk. E.G. See the database::database() constructor function. database::verifyindex int database::verifyindex(char *indexname,int depth=1, void (*progress)(int test,long rec)=0, int order=100); Description : This function will run through the entire database using the supplied index name which should already be attached to the database, and check that the index matches the records in the database. depth controls the thoroughness of the test. If depth is 1 (the default), then about 1000 records are checked per second (on a 33MHz 486), and the test checks that the index holds the same number of records as the database. If depth is 2, then the order of records is checked, if depth is 3, then every record in the database is found in the index. Depth 2 and 3 are intended mainly for debugging, and are too slow for normal use. progress is the optional address of a function which can show the progress of the index verify, and can be used for example, to show a bar chart of % checked. The function progress will be called every order records (as default order=100). The function is called with an integer test, which is the stage of the verify, and a long r, which is the record number. r runs from 1 to the selected depth. r runs from 1 to the number of records in the database for each stage. verifyindex returns: 0 if the index is OK. INCOMP if the index appears to end before all records have been read from the database, OORD if the records are out of order RECNOTFND if a record is not found in the index ERRINTREE if the index tree has an error in it INVIND if the index is not attached to the database, or there is an error in the index expression. E.G. database db("CLUB","NAME"); if (db->verifyindex("NAME)) // On error rebuild index { db->subindex("name"); db->buildindex("upper(name)","name"); db->addindex("name"); } field::Accessor Functions int field::getlen(); char *field::getname(); int field::getnumber(); int field::getrdp(); char field::gettype(); Description : These functions return various information about the field object to which they are applied. int field::getlen(); This returns the length of the field in characters. char *field::getname(); This returns a pointer to the name of the field. int field::getnumber(); This returns the number of the field. int field::getrdp(); This returns the number of characters to the right of the decimal place in a numeric field. char field::gettype(); This returns the single character field type which will be one of C,N,D,L, or M. These have the following meanings : C Character field N Numeric field D Date field L Logical field M Memo field E.G. #include field *fpt; database db("DATA",""); fpt=db.getfield("DATE"); cout << "Date field is number :" << fpt->getnumber(); cout << " and of type " << fpt->gettype(); See Also : record::record void record::record(class database &db, char *index=0); Description : The record constructor is called with a database to which the record object will belong. The object may then hold any record from that database, and the extraction functions may be used to obtain any of the fields from the record. The selection functions are used to read a required record into the object. Note that the constructor reads no information into the record, the record contents will be invalid until the select functions are used. index is the index to be used with this record. If no indexes are in use, or if NOIND is supplied as the index parameter then records are read from the database in the order that they were written (this is fastest). If the index is supplied as 0 then the first index attached to the database is used for this record, this will be attached either by the database constructor, or by the database::addindex() function. If the index is a name then this is the one used. The index name is the filename of the .NDX file without the .NDX extension so the index file "name.ndx" is referred to as "name". If the index cannot be opened then no index is associated with the record. E.G. #include database db("EQUIP","TITLE"); // Database constructor db.addindex("AGE"); // Age index is attached db.addindex("NAME"); // Name index is attached record rec(db); // Record uses "TITLE" index record rec1(db,NOIND); // Record with no index record rec2(db,"AGE"); // Record uses "AGE" index See Also : record::select(), database::addindex(), database::database() record::Accessor Functions int record::getdelstate(); long record::getrecnum(); char *record::indname(); int record::setdelstate(int state); Description : These functions return information about the record object. int record::getdelstate(); This function returns the delete state of the record either, either DEL or NOTDEL. long record::getrecnum(); This function returns the record number of the record object in the database. char *record::indname(); This function returns the index name used on this record, or 0 if none. void record::setdelstate(int state); This setes the delete state of the record, to deleted (state==DEL), or not deleted (state==NOTDEL). Note that the record in the dbf file is not updated until a record::write() function is executed. record::eval void record::eval(char *expr,void *result,int &rtype); void record::eval(void *result,int &rtype); Description : These functions evaluate an expression which is provided in ASCII (e.g. "age<12") and return a result. The expression is evaluated on the current record and values for fields in the expression are taken from the current record. When the function is called, the expression is tokenised and stored internally. If the same expression is to be evaluated more than once, then for the second and subsequent calls the second form of the expression may be used. This will result in faster evaluation, however the tokenised form of the expression is overwritten by any call to eval by any record, by the record::indchk() function, by the index building functions (which are called by database::addindex(), database::buildindex(), and by adding a record to an indexed database), and by the record select by expression function. The function returns 0 if there was an error in the evaluation in which case the owning database string ers may be checked for an ASCII representation of the error. On success the function returns 1. result points to an area of memory into which is written the result of the evaluation. rtype represents the type of the result: rtype Result Type result points to : 1 (OPINT) Numeric A double 2 (OPSTR) String A string 4 (OPDATE) Date An 8 character string in dBase data format 8 (OPLOG) Logical Single character string - F or T. Memo fields may be used freely in expression, however the expression evaluator maintains its own memory area which may overflow if results of string expressions are too long. Memo fields are stored in a buffer which may be modified by the evaluator, see the record::getfield function for a further discussion of this. E.G. // Print the age in years from the dob field // in the database (date of birth) and the current date. // Print for all those whose name begins with "A" database db("CLUB"); record rec(db); int sel=rec.select("name\"A\"",FIRST); while(!sel) // For all records { char result[128]; // Holds result int dum; // Dummy variable cout << "\n" << (char *)rec.getfield("name") << "is "; int rv=rec.eval("int((date()-dob/365.25)",result,dum); if (!rv) { cout << "Couldn't calculate result because :"; cout << db.ers; } else cout << *(double *)result << " years old"; sel=rec.select("name=\"A\"",NEXT); } record::getfield void record::getfield(int n,int trimflag=1); void record::getfield(char *name,int trimflag=1); Description : These functions return a pointer to (a copy of) the specified field. n is the field number (the first field is number 1), or the field can be specified by name. It should be noted that it is much faster to obtain a field by number than by name as the latter requres a search of all field names in the current data base. Note that the string returned is not guaranteed to be maintained through further calls to the database library functions, so a copy should be made if it is required to maintain the field value through other functions. The function returns a string of spaces if the field cannot be found, or if a record has not been read with one of the select functions. If trimflag is 1 then results are trimmed, that is trailing spaces are removed, if it is 0 then the field is returned as is. The return values of the function are the internal form held in dbf files, thus the string will have the following types: FIELD TYPE FORMAT Character Zero terminated string form of field. Numeric ASCII representation of number, use atoi() or atof() to obtain number in integer or floating point form. Date String form of date : YYYYMMDD, thus 19/11/1965 is held as 19651119. Use the gettime() function to return an ANSI time in seconds which may be used with ANSI C time functions. Logical The return value is a single character which holds T,t,Y,y for TRUE and F, f, N, or n for FALSE. Memo Returns a pointer to a buffer which holds the memo in standard string format, see below. Memos: Memo fields are held in a buffer which is initially 512 bytes long, but is increased in length as required as bigger memos are found. Not that there is only one memo buffer per database, and it is overwritten every time that a memo field is accessed from any record. Expression which act on memo fields may also modify this buffer. Thus it is important that if it is desired to keep the memo field intact through other database function calls then the buffer should be copied. The buffer (and therefore any memo field) has a maximum length of 64k. Empty memos return a null string. E.G. see record::select record::indchk int record::indchk(char *expr,int &rtype); Description: This function checks the supplied expression and returns the length of the result in an index expression (e.g. trim(name) has a length which is the same as the length of the name field). If the expression is not valid for an index (e.g. it's result length exceeds 100 characters, the result is logical, or there is an error in the expression etc.) then the function returns 0, and the ers string in the database is set to the error report. rtype is set to the type of the expression which takes the same values as the rtype parameter in the record::eval() function. E.G. database db("Names","CNAME"); record rec(db); int rt; int indval=rec.indchk(exprt,rt); if (!indval) printf("\n %s is not a valid index because %s",exprt,rec.db.ers); record::seldbf int record::seldbf(long n); Description: This function sets the record to record number n in the dbf file regardless of any index file in current use, or the state of the record (deleted or not). Note that the first record is record 1. It returns 0 on success, or CANTSEL if n is out of range. After this function the record::select() functions may give unpredictable results unless the record::select(..FIRST) function is used. See Also: record::select() record::select int record::select(long n,int type=NOTDEL); int record::select(int fieldnum,int value,long n,int type=NOTDEL); int record::select(int fieldnum,char *value,long n,int type=NOTDEL); int record::select(int fieldnum,void *value,int (*cmp)(void *,void *),long n,int type=NOTDEL); int record::select(char *expr,long n,int type=NOTDEL); Description : These functions update the record structure to contain the selected record from the database. The first function record::select(n) simply selects a record without regard to the contents of the record, the others select record by some criteria. The type parameter may be supplied as NOTDEL, DEL or ALL, this defines whether deleted, not deleted, or all records which match the criteria are retrieved. The second form selects records where the specified field number which is assumed to be numeric, matches the supplied integer value. The third form selects record where the specified field number matches the supplied string. The fourth form allows the user to supply a function which searches for records. The function takes two pointers, the first will be supplied as a string from the record (this is the value of the specified field), the second pointer is to the value supplied to the function.The function should return 0 if it has found a valid record. The final form takes a dBase expression which should return a logical value (e.g. "age<12") and returns all records for which the expression returns a True value. If the expression is supplied as "" then the last expression which was evaluated is used. (Note that any library function which uses the expression evaluator will modify the last expression). If an error occurs in evaluation or a non-logical value is returned, then this function returns CANTSEL. The functions read a record from the database according to the value supplied in n, and then allows the user to access the fields in that record using the record accessor functions. The function returns 0 is it was successful, and the error CANTSEL if it failed. If an index is in use then the record order specified in the index file is used by the select functions. If n is greater than 0 then it is the record number to be loaded, note that the first record is record number 1. The record number n is the nth record which matches the selection criteria, not the record number in the dbf file, use the record::seldbf() function is select the nth record in the dbf file. If n is less than 0 then it takes the values FIRST, LAST, NEXT or PREVIOUS. These values have the following meanings: FIRST The first record which matches the criteria is loaded. LAST The last record is loaded. NEXT The next record in sequence is loaded. PREVIOUS The previous record in sequence is loaded. E.G. #include int compfirst(void *,void *); /*******************************/ /* Print all names in database */ void main() { database db("MEMBERS","SERIAL"); record rec(db); int sel; sel=rec.select(FIRST); while (!sel) { printf("%s\n",rec.getfield("NAME")); sel=rec.select(NEXT); } /*********************************************/ /* Find all members whose name begins with C */ fieldnum=(db.getfield("NAME"))->number; int rv=rec.select(fieldnum,"C",compfirst,FIRST); while(!rv) { printf("%s\n",rec.getfield(fieldnum)); rv=rec.select(fieldnum,"C",compfirst,NEXT); } } /***************************************************/ /* This function compares two strings and returns */ /* zero if the first characters are the same */ int compfirst(void *name,void *value) { return(*(char *)name!=*(char *)value); } See Also : record::getfield() record::selkey int record::selkey(char *value,int type=NOTDEL); int record::selkey(double value,int type=NOTDEL); int record::selkey(); Description : This function selects the record by the index key attached to the record. value is the key value to find and is the result of the index expression on the record which will be matched to the record to find it in the database. type is either DEL, NOTDEL or ALL and has the same meanings as in the record::select() functions. The final form of the function (record::selkey()) selects records with the same key as supplied previously. Selecting by key (when known) is much faster than using the record::select() functions, although the right index should be used. It returns with 0 on success, NOKEY if record key could not be found and in this case will select the record with the next highest key, if there is no higher key then it returns NOKEYHIGHER, and selects the last record by index. If there is no index it returns CANTSEL. The record::select() functions may be used with NEXT and PREVIOUS parameters after this function is used if required. E.G. // Find all records where name starts with B record rec(*db,"Name"); // Use name index int sel=rec.selkey("B"); while(!sel) { printf("\n%s",rec.getfield("name")); sel=rec.selkey(); } record::setfield int record::setfield(char *name,char *value); int record::setfield(char *name,double value); int record::setfield(int fieldnum,char *value); int record::setfield(int fieldnum,double value); Description : Update a field in the record. The first two forms use the field name to set the field, the second two forms use fieldnum as the number of the field. The various forms of the function convert the argument types to a string and write this to the field. Date, Logical, Memo and Character fields should use the char * (as the 2nd parameter) form of the function, Numeric fields will use the double form. Date and Logical field values should be in dBase format (the getdate() function, described in the Functions part of this document, is useful here). Note that the record class is updated, but the database is not updated until a record::write() function is executed. The function returns 0 if the field is set to the value successfully, and RECNOTSET if an error occurred. The first field is field number 1 when the numeric version is used. Memo Fields: Memo fields must provide a pointer to a buffer which should remain valid until the record is written. The old buffer in the memo file is overwritten if the new memo field will fit in the same space as the old one. E.G. #include // Undelete all deleted records, and // update a field which indicates the number of modifications // to the record, this is field number 17 database db("Names"); int rv=rec.select(FIRST,DEL); // Select 1st deleted record while(!rv) { rec.setdelstate(NOTDEL); int nwrites=atoi(rec.getfield(17)); rec.setfield(17,nwrites+1); rec.write(); rv=rec.select(NEXT,DEL); } record::write int record::write(int type=OVER); Description : Write an updated record back to the dbf file. type is either OVER or NEW. If type is OVER then the record is written back on top of the record which was originally read, this form is used for updating records. If type is NEW then the record is added at the end of the dbf file. The function returns 0 if it was successful, and NOWRUNR if the application has attempted to overwrite an unread record, and WRFAIL if the write failed for some other reason. The function also updates any indexes attached to the current database, and it is not until this function is executed that these indexes are updated. The function checks to see if any other record classes attached to the database have the same record selected, if so the information is copied to these records updating them. E.G. See record::setfield() Appendix 1 - Format of database files DBase_3,_File_Format DBF File The DBF file holds all the records in the database together with global information on fields and records. It consists of 3 parts : 1) The file header which is 32 bytes long. 2) The field definitions each of which is 32 bytes long, the last field is followed by the byte 0DH. 3) The records each of which follows the last. File Header This is 32 bytes long, the contents are as follows : Byte 0 Bits 0-2 Version of dBase, Bit 7 flags Memo file for this database if set Bytes 1-3 Year, Month and Day (in hex) of last modification to database. Bytes 4-7 Number of records. Bytes 7-8 The offset of the first record from the start of the file. Bytes 10-11 The record length including the start byte. Field Definitions Each definition is 32 bytes long, the contents of each field are defined as follows: Bytes 0-10 Zero terminated, upper case field name, max 10 characters. Byte 11 Type of the field, C, N, L, D, or M. Byte 16 The length of the field in characters as seen on screen. Byte 17 The number of digits right of decimal point for numeric fields, 0 for all others. Records Each record is stored in order of the field contents. The first byte of each record is either a space ' ' for an undeleted record, or a '*' for a deleted record. Formats for all data stored in the record is ASCII except for date and memo fields : Date These fields are stored in 8 digit ascii, year then month then day to enable simple sorting. eg 27th September 1991 is stored as '19910927'. Memo These fields are stored as 10 ascii digits which show the cluster number of the memo in the dbt file. Thus a value of '0000000023' is at location 23*512 in the memo file. This field is all 0's for no memo on this record. NDX, Index Files These files store the indexes for the database. They consist of a header cluster, and a complex hierarchical index structure which is based on 512 byte clusters. Header The header consists of the first cluster of the index file (512 bytes). It contains information about the index file. Bytes 0-3 Cluster number of the first cluster of the index file which is at the top of the tree. Bytes 4-7 Number of clusters in the file. Byte 9 Type of result, D,C or N, Logical index expressions are invalid. Bytes 12-13 The size of the result of the dBase index expression used for this file, in bytes. Bytes 14-15 The maximum number of index records per cluster. Bytes 16-17 The result type, 0=character, 1=numeric. Bytes 18-19 The total length of the index expression plus its pointers. ie the value in bytes 12-13 + 8+n, where n is chosen to round up to the nearest 4 bytes. Byte 24+ The dBase index expression used on this file, which is zero terminated. Index Cluster Format Each cluster consists of a number of records. Each record starts with two 4 byte numbers, the block number and the record number. Following this is the record itself which is the result of the dBase index expression. The cluster starts with the number of records in it, or the number of records-1 for block clusters, this is also a 4 byte number. There are two types of cluster : A record cluster contains index records where the block number is set to 0, and the record number is set to the record number in the dbf file for this index. Following the last record is a 00000000 byte sequence. The second type is a block cluster which pointes in a tree like fashion to another block cluster, or to a record cluster. This type has as its records the value of the index record of the last item in the block to which it points. A block cluster has the record number set to 0, and the block number set to the cluster number of the block to which it refers. Memo Files Memo files are organised into 512 byte clusters. Each memo takes up an integral number of clusters, and the text ends in the double byte 1A, 1A. The first 4 bytes of the file hold the number of the next free block, and the rest of block 0 is free for user supplied comments. Block 1 is the first block available for use as a memo. ÿÿ